#include <fs.h>
#include <sys/sysdef.h>
#include <fflags.h>
#include <process.h>
#include <status.h>
#include <mm/valloc.h>
#include <mm/slab.h>
#include <log.h>
#include <ds/mutex.h>
#include <ds/lru.h>
#include <lib/math.h>
#include <fs/dirent.h>

kmem_cache_t* dentry_kmem_cache;
kmem_cache_t* inode_kmem_cache;
kmem_cache_t* file_kmem_cache;
kmem_cache_t* sb_kmem_cache;
kmem_cache_t* fd_kmem_cache;

dentry_t* fs_root;
lru_t* dentry_lru, * inode_lru;

hbkt_t* dentry_cache;

hstr_t fs_path_dot = HSTR(".", 1);
hstr_t fs_path_dotdot = HSTR("..", 2);
hstr_t fs_path_empty = HSTR("", 0);

int fs_dentry_destory(lnode_t* this) {
    return ENOSET;
}

int fs_inode_destory(lnode_t* this) {
    return ENOSET;
}

void fs_init() {
    dentry_kmem_cache = new_kmem_cache("dentry_kmem_cache", sizeof(dentry_t), 1, 0);
    inode_kmem_cache = new_kmem_cache("inode_kmem_cache", sizeof(inode_t), 1, 0);
    file_kmem_cache = new_kmem_cache("file_kmem_cache", sizeof(file_t), 1, 0);
    sb_kmem_cache = new_kmem_cache("sb_kmem_cache", sizeof(sb_t), 1, 0);
    fd_kmem_cache = new_kmem_cache("fd_kmem_cache", sizeof(fd_t), 1, 0);

    dentry_cache = vzalloc(FS_HASHTABLE_SIZE * sizeof(hbkt_t));

    dentry_lru = lru_init(fs_dentry_destory);
    inode_lru = lru_init(fs_inode_destory);

    hstr_rehash(&fs_path_dot);
    hstr_rehash(&fs_path_dotdot);

    fs_root = fs_alloc_dentry(NULL, &fs_path_empty);
    fs_root->parent = fs_root;
    atomic_fetch_add(&fs_root->ref_count, 1);

    fs_install_default();
}

int fs_alloc_fd(int* fd) {
    for (size_t i = 0; i < FS_FD_MAX; i++) {
        if (!cur_proc->fd_table->fds[i]) {
            *fd = i;
            return 0;
        }
    }
    return EMFILE;
}

int fs_dup_fd(fd_t* old, fd_t** new) {
    int errno = 0;
    fd_t* copied = take_obj(fd_kmem_cache);

    memcpy(copied, old, sizeof(fd_t));

    atomic_fetch_add(&old->file->ref_count, 1);

    *new = copied;

    return RET_STATUS(errno);
}

int fs_open(dentry_t* dentry, file_t** file) {
    if (!dentry->inode || !dentry->inode->iops->open) {
        return ENOSET;
    }

    inode_t* inode = dentry->inode;

    mutex_lock(&inode->lock);
    file_t* ofile = take_obj(file_kmem_cache);
    ofile->inode = inode;
    ofile->dentry = dentry;
    ofile->ref_count = ATOMIC_VAR_INIT(1);
    ofile->ops = inode->fops;

    if ((inode->type & FS_FILE) && !inode->pcache) {
        pcache_t* pcache = valloc(sizeof(pcache_t));
        pcache_init(pcache);
        pcache->parent = inode;
        inode->pcache = pcache;
    }

    int errno = inode->iops->open(inode, ofile);
    if (errno) {
        release_obj(file_kmem_cache, ofile);
    } else {
        atomic_fetch_add(&dentry->ref_count, 1);
        inode->open_count++;
        mount_busy(dentry->mnt);
        *file = ofile;
    }
    mutex_unlock(&inode->lock);
    return errno;
}
int fs_close(file_t* file) {
    int errno = 0;
    if (file->ref_count > 1) {
        atomic_fetch_sub(&file->ref_count, 1);
    } else if (!(errno = file->ops->close(file))) {
        atomic_fetch_sub(&file->dentry->ref_count, 1);
        file->inode->open_count--;

        if (file->inode->open_count == 0) {
            release_obj(file_kmem_cache, file);
        }
    }
    return errno;
}

static int try_find_file(const char* path, dentry_t** dentry, dentry_t** file, int flags) {
    char name_buf[FS_NAME_MAX + 1];
    hstr_t name = HSTR(name_buf, 0);

    int errno = fs_path_walk(path, cur_proc->cwd, dentry, &name, FS_WALK_FDPARENT);
    if (errno)
        return errno;

    errno = fs_path_walk(name.val, *dentry, file, NULL, 0);
    if (errno != ENOENT || !(flags & F_CREATE))
        return errno;

    dentry_t* parent = *dentry;
    dentry_t* new_file = fs_alloc_dentry(parent, &name);

    if (!new_file) {
        return ENOMEM;
    }

    mutex_lock(&parent->lock);
    if (!(errno = parent->inode->iops->create(parent->inode, new_file))) {
        *file = new_file;
    } else {
        fs_free_dentry(new_file);
    }
    mutex_unlock(&parent->lock);

    return errno;
}

int fs_getfd(int fd, fd_t** fd_) {
    if (0 <= fd && fd < FS_FD_MAX && (*fd_ = cur_proc->fd_table->fds[fd])) {
        return 0;
    }
    return EBADF;
}

void fs_ref_dentry(dentry_t* dentry) {
    atomic_fetch_add(&dentry->ref_count, 1);
    mount_busy(dentry->mnt);
}

void fs_unref_dentry(dentry_t* dentry) {
    atomic_fetch_sub(&dentry->ref_count, 1);
    mount_free(dentry->mnt);
}

int fs_chdir(struct process* process, dentry_t* dentry) {
    int errno = 0;

    lock_dentry(dentry);

    if (!(dentry->inode->type & FS_DIR)) {
        errno = ENOTDIR;
        goto out;
    }

    if (process->cwd) {
        fs_unref_dentry(process->cwd);
    }

    process->cwd = dentry;
    fs_ref_dentry(dentry);

    unlock_dentry(dentry);
out:
    return errno;
}

int fs_get_path(dentry_t* dentry, char* buf, size_t size, int depth) {
    if (!dentry) {
        return 0;
    }

    if (depth > 64) {
        return ENAME2LONG;
    }

    size_t len = 0;

    if (dentry->parent != dentry) {
        len = fs_get_path(dentry->parent, buf, size, depth + 1);
    }

    if (len >= size) {
        return len;
    }

    if (!len || buf[len - 1] != '/') {
        buf[len++] = '/';
    }

    size_t cpy_size = MIN(dentry->name.len, size - len);
    strncpy(buf + len, dentry->name.val, cpy_size);
    len += cpy_size;

    return len;
}

__DEF_SYSCALL2(int, open, const char*, path, int, flags) {
    int errno, fd;
    dentry_t* dentry, * file;
    file_t* ofile;

    errno = try_find_file(path, &dentry, &file, (flags & F_CREATE) ? F_CREATE : 0);

    if (errno || (errno = fs_open(file, &ofile))) {
        return RET_STATUS(errno);
    }

    inode_t* inode = ofile->inode;
    if (!(errno = fs_alloc_fd(&fd))) {
        fd_t* fd_ = take_obj(fd_kmem_cache);
        ofile->f_pos = (flags & F_APPEND) ? inode->fsize : 0;
        fd_->file = ofile;
        fd_->flags = flags;
        cur_proc->fd_table->fds[fd] = fd_;
        return fd;
    }

    return RET_STATUS(errno);
}

__DEF_SYSCALL1(int, close, int, fd) {
    fd_t* fd_;
    int errno = 0;
    if ((errno = fs_getfd(fd, &fd_))) {
        goto out;
    }

    if ((errno = fs_close(fd_->file))) {
        goto out;
    }

    release_obj(fd_kmem_cache, fd_);
    cur_proc->fd_table->fds[fd] = 0;

out:
    return RET_STATUS(errno);
}

__DEF_SYSCALL3(int, write, int, fd, char*, buf, int, count) {
    int errno;
    fd_t* fd_;
    if ((errno = fs_getfd(fd, &fd_))) {
        goto out;
    }

    file_t* file = fd_->file;
    if (file->dentry->mnt->flags & FS_MNT_READONLY) {
        errno = EROFS;
        goto out;
    }

    if (file->inode->type & FS_DIR) {
        errno = EISDIR;
        goto out;
    }

    lock_inode(file->inode);

    errno = file->ops->write(file->inode, buf, count, file->f_pos);
    if (errno > 0) {
        file->f_pos += errno;
        unlock_inode(file->inode);
        return RET_STATUS(errno);
    }
    unlock_inode(file->inode);
out:
    return RET_STATUS(errno);
}

__DEF_SYSCALL3(int, read, int, fd, char*, buf, int, count) {
    int errno = 0;
    fd_t* fd_;
    if ((errno = fs_getfd(fd, &fd_))) {
        goto done;
    }

    file_t* file = fd_->file;
    if ((file->inode->type & FS_DIR)) {
        errno = EISDIR;
        goto done;
    }

    lock_inode(file->inode);
    fd_->flags |= F_DIRECT;
    if ((file->inode->type & FS_CHAR) || (fd_->flags & F_DIRECT)) {
        errno = file->ops->read(file->inode, buf, count, file->f_pos);
    } else {
        // TODO: page cache
    }

    if (errno > 0) {
        file->f_pos += errno;
        unlock_inode(file->inode);
        return RET_STATUS(errno);
    }

    unlock_inode(file->inode);

done:
    return RET_STATUS(errno);
}

__DEF_SYSCALL1(int, chdir, const char*, path) {
    int errno = 0;
    dentry_t* dentry;
    if ((errno = fs_path_walk(path, cur_proc->cwd, &dentry, NULL, 0))) {
        goto out;
    }

    errno = fs_chdir(cur_proc, dentry);
out:
    return RET_STATUS(errno);
}

__DEF_SYSCALL1(int, fchdir, int, fd) {
    int errno = 0;
    fd_t* fd_;

    if ((errno = fs_getfd(fd, &fd_))) {
        goto out;
    }

    errno = fs_chdir(cur_proc, fd_->file->dentry);
out:
    return RET_STATUS(errno);
}

__DEF_SYSCALL1(int, fsync, int, fd) {
    int errno;
    fd_t* fd_;
    if (!(errno = fs_getfd(fd, &fd_))) {
        goto out;
    }

    file_t* file = fd_->file;
    if (file->dentry->mnt->flags & FS_MNT_READONLY) {
        errno = EROFS;
        goto out;
    }

    lock_inode(file->inode);

    // page cache commit 
    errno = ENOSET;
    if (file->ops->sync) {
        errno = file->ops->sync(file);
    }

    unlock_inode(file->inode);
out:
    return RET_STATUS(errno);
}

__DEF_SYSCALL2(int, readdir, int, fd, dirent_t*, dirent) {
    fd_t* fd_;
    int errno;

    if ((errno = fs_getfd(fd, &fd_))) {
        goto done;
    }

    inode_t* inode = fd_->file->inode;

    lock_inode(inode);

    if ((inode->type & FS_DIR)) {
        errno = 1;
        if (dirent->d_offset == 0) {
            dirent_assign(dirent, fs_path_dot.val, fs_path_dot.len, DT_DIR);
        } else if (dirent->d_offset == 1) {
            dirent_assign(dirent, fs_path_dotdot.val, fs_path_dotdot.len, DT_DIR);
        } else {
            if ((errno = fd_->file->ops->readdir(fd_->file, dirent)) != 1) {
                unlock_inode(inode);
                goto done;
            }
        }
        dirent->d_offset++;
    } else {
        errno = ENOTDIR;
    }

    unlock_inode(inode);

done:
    return RET_STATUS(errno);
}

__DEF_SYSCALL1(int, dup, int, fd) {
    int errno, newfd;
    fd_t* fd_, * newfd_;
    if ((errno = fs_getfd(fd, &fd_))) {
        goto out;
    }

    if ((errno = fs_alloc_fd(&newfd))) {
        goto out;
    }

    newfd_ = take_obj(fd_kmem_cache);
    memcpy(newfd_, fd_, sizeof(fd_t));

    atomic_fetch_add(&fd_->file->ref_count, 1);

    cur_proc->fd_table->fds[newfd] = newfd_;
    return newfd;
out:
    return RET_STATUS(errno);
}

__DEF_SYSCALL2(int, dup2, int, oldfd, int, newfd) {
    if (newfd == oldfd) {
        return newfd;
    }

    int errno;
    fd_t* oldfd_, * newfd_;
    if ((errno = fs_getfd(oldfd, &oldfd_))) {
        goto done;
    }

    if (0 <= newfd && newfd < FS_FD_MAX) {
        errno = EBADF;
        goto done;
    }

    newfd_ = cur_proc->fd_table->fds[newfd];
    if (newfd_ && (errno = fs_close(newfd_->file))) {
        goto done;
    }

    newfd_ = take_obj(fd_kmem_cache);
    memcpy(newfd_, oldfd_, sizeof(fd_t));
    atomic_fetch_add(&oldfd_->file->ref_count, 1);

    cur_proc->fd_table->fds[newfd] = newfd_;

    return newfd;
done:
    return RET_STATUS(errno);
}

__DEF_SYSCALL2(char*, getcwd, char*, buf, int, size) {
    int errno = 0;
    char* ptr = NULL;
    if (!cur_proc->cwd) {
        buf[0] = '/';
        errno = 1;
        goto out;
    }

    if ((u32)size < cur_proc->cwd->name.len + 1) {
        errno = ERANGE;
        goto out;
    }

    errno = fs_get_path(cur_proc->cwd, buf, size - 1, 0);

    if (errno >= size - 1) {
        errno = ERANGE;
        goto out;
    }

    buf[errno + 1] = '\0';
    ptr = buf;
out:
    cur_proc->k_status = errno;
    return ptr;
}

__DEF_SYSCALL2(int, rename, const char*, oldpath, const char*, newpath) {
    return ENOSET;
}

__DEF_SYSCALL1(int, mkdir, const char*, path) {
    int errno;
    dentry_t* dir, * parent;
    char name_value[FS_NAME_MAX + 1];
    hstr_t name = HHSTR(name_value, 0, 0);

    if ((errno = fs_path_walk(path, cur_proc->cwd, &parent, &name, FS_WALK_FDPARENT))) {
        goto out;
    }

    if (parent->inode->type != FS_DIR) {
        errno = ENOTDIR;
        goto out;
    }

    if (parent->mnt->flags & FS_MNT_READONLY
        || parent->sb->fs->type & FS_TYPE_READONLY
        ) {
        errno = EROFS;
        goto out;
    }

    if (!parent->inode->iops->mkdir) {
        errno = ENOSET;
        goto out;
    }

    if (!(dir = fs_alloc_dentry(parent, &name))) {
        errno = ENOMEM;
        goto out;
    }

    lock_dentry(parent);
    lock_inode(parent->inode);

    if (!(errno = parent->inode->iops->mkdir(parent->inode, dir))) {
        fs_dcache_add(parent, dir);
    } else {
        fs_free_dentry(dir);
    }

    unlock_inode(parent->inode);
    unlock_dentry(parent);

out:
    return RET_STATUS(errno);
}

__DEF_SYSCALL1(int, rmdir, const char*, path) {
    int errno;
    dentry_t* dentry;
    if ((errno = fs_path_walk(path, cur_proc->cwd, &dentry, NULL, 0))) {
        return RET_STATUS(errno);
    }
    lock_dentry(dentry);

    if (dentry->mnt->flags & FS_MNT_READONLY) {
        errno = EROFS;
        goto out;
    }

    if (dentry->sb->fs->type & FS_TYPE_READONLY) {
        errno = EROFS;
        goto out;
    }

    if (dentry->ref_count > 1 || dentry->inode->open_count) {
        errno = EBUSY;
        goto out;
    }

    if (dentry->inode->type != FS_DIR) {
        errno = ENOTDIR;
        goto out;
    }

    if (!list_empty(&dentry->children)) {
        errno = ENOTEMPTY;
        goto out;
    }

    dentry_t* parent = dentry->parent;
    if (!parent) {
        errno = EINVAL;
        goto out;
    }

    lock_dentry(parent);
    lock_inode(parent->inode);

    if (!(errno = parent->inode->iops->rmdir(parent->inode, dentry))) {
        fs_dcache_remove(dentry);
    }

    unlock_inode(parent->inode);
    unlock_dentry(parent);
out:
    unlock_dentry(dentry);
    return RET_STATUS(errno);
}

__DEF_SYSCALL3(int, lseek, int, fd, int, offset, int, option) {
    int errno = 0;
    fd_t* fd_;
    if ((errno = fs_getfd(fd, &fd_))) {
        goto out;
    }

    file_t* file = fd_->file;

    if (!file->ops->seek) {
        errno = ENOSET;
        goto out;
    }

    lock_inode(file->inode);

    int overflow = 0;
    int fpos = file->f_pos;
    switch (option) {
    case F_SEEK_CUR:
        overflow = __builtin_sadd_overflow((int)file->f_pos, offset, &fpos);
        break;
    case F_SEEK_END:
        overflow = __builtin_sadd_overflow((int)file->inode->fsize, offset, &fpos);
        break;
    case F_SEEK_SET:
        fpos = offset;
        break;
    }

    if (overflow) {
        errno = ESPIPE;
    } else if (!(errno = file->ops->seek(file->inode, offset))) {
        file->f_pos = fpos;
    }

    unlock_inode(file->inode);
out:
    return RET_STATUS(errno);
}

__DEF_SYSCALL3(int, realpathof, int, fd, char*, buf, size_t, size) {
    int errno;
    fd_t* fd_;
    if ((errno = fs_getfd(fd, &fd_))) {
        goto out;
    }

    errno = fs_get_path(fd_->file->dentry, buf, size, 0);
    if (errno >= 0) {
        return errno;
    }
out:
    return RET_STATUS(errno);
}